﻿// <copyright file="TimesPeople.cs" company="Microsoft Corporation">
// Copyright (c) 2009 Microsoft Corporation All Rights Reserved
// </copyright>
// <author>Michael S. Scherotter</author>
// <email>mischero@microsoft.com</email>
// <date>2009-05-06</date>
// <summary>TimesPeople class definition</summary>

namespace NYTimes.Core
{
    using System;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.Linq;
    using System.Net;
#if SILVERLIGHT
#else
    using System.Web;
#endif
    using System.Windows;
#if WINDOWS_PHONE
#elif SILVERLIGHT
    using System.Windows.Browser;
#endif
    using System.Windows.Media.Imaging;
    using System.Xml.Linq;
    using NYTimes.Data;
    using Synergist;

    /// <summary>
    /// TimesPeople CLR Class.  See 
    /// <a href="http://developer.nytimes.com/docs/timespeople_api">this article</a> for more 
    /// information.
    /// </summary>
    public abstract class TimesPeople : INotifyPropertyChanged
    {
        #region Fields
        /// <summary>
        /// The Application.Resources key for the API Key
        /// </summary>
        private const string NYTTimesPeopleKey = "NYTTimesPeopleKey";

        /// <summary>
        /// The API Version
        /// </summary>
        private const string Version = "v1";

        /// <summary>
        /// The base API URL
        /// </summary>
        private const string BaseApi = "http://api.nytimes.com/svc/timespeople/api/{0}/user";

        /// <summary>
        /// 8 QPS rate limiting
        /// </summary>
        private static RateLimiting limiting = new RateLimiting("NYTTimesPeople", 8);

        /// <summary>
        /// the user's email address
        /// </summary>
        private string emailAddress;

        /// <summary>
        /// the TimesPeople API Key
        /// </summary>
        private string apiKey;

        /// <summary>
        /// the TimesPeople User ID
        /// </summary>
        private long userId;

        /// <summary>
        /// are the results loading?
        /// </summary>
        private bool isLoading;

        /// <summary>
        /// The Web client
        /// </summary>
        private WebClient client;
        #endregion

        /// <summary>
        /// Initializes a new instance of the TimesPeople class.
        /// </summary>
        protected TimesPeople()
        {
            if (Application.Current.Resources.Contains(NYTTimesPeopleKey))
            {
                this.apiKey = Application.Current.Resources[NYTTimesPeopleKey] as string;
            }
        }

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Property changed event
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the TimesPeople API key
        /// </summary>
        [Description("Gets or sets the TimesPeople API key")]
        public string ApiKey
        {
            get
            {
                return this.apiKey;
            }

            set
            {
                if (this.apiKey != value)
                {
                    this.apiKey = value;

                    this.FetchResults();
                }
            }
        }

        /// <summary>
        /// Gets a value indicating whether the results are loading
        /// </summary>
        [Description("Gets a value indicating whether the results are loading")]
        public bool IsLoading
        {
            get
            {
                return this.isLoading;
            }

            private set
            {
                if (this.isLoading != value)
                {
                    this.isLoading = value;

                    this.TriggerPropertyChanged("IsLoading");
                }
            }
        }

        /// <summary>
        /// Gets the logo for the New York times
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1822", Justification = "This is exposed to a Silverlight application.")]
        public BitmapImage Logo
        {
            get
            {
                var streamResource = Application.GetResourceStream(
    new Uri("NYTimesSilverlightKit;component/Images/Logo.png", UriKind.Relative));

                var image = new BitmapImage();

#if SILVERLIGHT
                image.SetSource(streamResource.Stream);
#else
                image.StreamSource = streamResource.Stream;
#endif

                return image;
            }
        }

        /// <summary>
        /// Gets or sets the Email Address for the TimesPeople user
        /// </summary>
        [Description("Gets or sets the Email Address for the TimesPeople user.")]
        public string EmailAddress
        {
            get
            {
                return this.emailAddress;
            }

            set
            {
                if (this.emailAddress != value)
                {
                    this.emailAddress = value;

                    this.FetchUserId();
                }
            }
        }

        /// <summary>
        /// Gets or sets the User Id
        /// </summary>
        [Description("Gets or sets the TimesPeople User Id.")]
        public long UserId
        {
            get
            {
                return this.userId;
            }

            set
            {
                if (this.userId != value)
                {
                    this.userId = value;

                    this.TriggerPropertyChanged("UserId");

                    this.FetchResults();
                }
            }
        }

        /// <summary>
        /// Gets the data type
        /// </summary>
        public abstract string DataType
        {
            get;
        }

        #endregion

        #region Methods
        /// <summary>
        /// Parse the result set
        /// </summary>
        /// <param name="resultSet">the result_set element</param>
        public abstract void ParseResultSet(XElement resultSet);

        /// <summary>
        /// Error handler
        /// </summary>
        /// <param name="failure">the error from the web client</param>
        public abstract void OnException(Exception failure);

        /// <summary>
        /// Create a <see cref="NYTimes.Data.Profile"/> object
        /// </summary>
        /// <param name="profileElement">a profile element</param>
        /// <returns>a new profile object</returns>
        protected static NYTimes.Data.Profile CreateProfile(XContainer profileElement)
        {
            if (profileElement == null)
            {
                throw new ArgumentNullException("profileElement");
            }

            var userUrl = profileElement.Element("user_url");

            var profile = new NYTimes.Data.Profile()
            {
                Bio = profileElement.Element("user_bio").Value,
                DisplayName = profileElement.Element("user_displayname").Value,
                Followers = int.Parse(profileElement.Element("followers_count").Value, CultureInfo.InvariantCulture),
                Following = int.Parse(profileElement.Element("following_count").Value, CultureInfo.InvariantCulture),
                Location = profileElement.Element("location").Value,
                PictureUrl = profileElement.Element("user_pic_url") == null ? null : new Uri(profileElement.Element("user_pic_url").Value, UriKind.Absolute),
                Url = userUrl == null || string.IsNullOrEmpty(userUrl.Value) ? null : new Uri(userUrl.Value, UriKind.Absolute),
                UserId = long.Parse(profileElement.Element("user_id").Value, CultureInfo.InvariantCulture)
            };
            return profile;
        }

        /// <summary>
        /// Parses the results for a collection of user profiles
        /// </summary>
        /// <param name="resultSet">the result set</param>
        /// <param name="collection">a collection of activities</param>
        protected static void ParseUsers(XContainer resultSet, ObservableCollection<Profile> collection)
        {
            if (resultSet == null)
            {
                throw new ArgumentNullException("resultSet");
            }

            if (collection == null)
            {
                throw new ArgumentNullException("collection");
            }

            var profiles = from profile in resultSet.Element("results").Elements("profile")
                           select CreateProfile(profile);

            collection.Clear();

            foreach (var profile in profiles)
            {
                collection.Add(profile);
            }
        }

        /// <summary>
        /// Parses the results for a collection of activities
        /// </summary>
        /// <param name="resultSet">the result set</param>
        /// <param name="collection">a collection of activities</param>
        protected static void ParseActivities(XContainer resultSet, ObservableCollection<Activity> collection)
        {
            if (resultSet == null)
            {
                throw new ArgumentNullException("resultSet");
            }

            if (collection == null)
            {
                throw new ArgumentNullException("collection");
            }

            var activities = from activity in resultSet.Element("results").Elements("activity")
                             select new Activity()
                             {
                                 DateUpdated = DateTime.Parse(activity.Element("date_updated").Value, CultureInfo.InvariantCulture),
                                 Id = long.Parse(activity.Element("activity_id").Value, CultureInfo.InvariantCulture),
                                 Object = activity.Element("object").Value,
                                 ObjectDescription = Uri.UnescapeDataString(activity.Element("object_desc").Value),
                                 ObjectNote = string.IsNullOrEmpty(activity.Element("object_note").Value) ? null : activity.Element("object_note").Value,
                                 ObjectThumbnailUrl = string.IsNullOrEmpty(activity.Element("object_thumbnail_url").Value) ? null : new Uri(activity.Element("object_thumbnail_url").Value, UriKind.Absolute),
                                 ObjectType = activity.Element("object_type").Value,
                                 ObjectUrl = new Uri(activity.Element("object_url").Value, UriKind.Absolute),
                                 UserDisplayName = activity.Element("user_displayname").Value,
                                 UserId = long.Parse(activity.Element("user_id").Value, CultureInfo.InvariantCulture),
                                 UserPictureUrl = string.IsNullOrEmpty(activity.Element("user_pic_url").Value) ? null : new Uri(activity.Element("user_pic_url").Value, UriKind.Absolute),
                                 Verb = activity.Element("verb").Value
                             };

            collection.Clear();

            foreach (var activity in activities)
            {
                collection.Add(activity);
            }
        }

        /// <summary>
        /// Trigger a property changed event
        /// </summary>
        /// <param name="propertyName">the property name</param>
        protected void TriggerPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// Parse the XML Results
        /// </summary>
        /// <param name="xml">the XML results</param>
        protected void Parse(string xml)
        {
            if (string.IsNullOrEmpty(xml))
            {
                return;
            }

            var doc = XDocument.Parse(xml);

            var resultSet = doc.Element("result_set");

            this.ParseResultSet(resultSet);
        }

        #endregion

        #region Implementation

        /// <summary>
        /// Fetch the user ID
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Not relevant for Silverlight"), 
        System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "not relevant for Silverlight.")]
        private void FetchUserId()
        {
            if (string.IsNullOrEmpty(this.ApiKey))
            {
                return;
            }

            var email = this.EmailAddress.Trim().ToLower(CultureInfo.InvariantCulture);

            var hash = Synergist.MD5Core.GetHashString(email).ToLower(CultureInfo.InvariantCulture);

            var uriString = string.Format(CultureInfo.InvariantCulture, BaseApi, Version);

            uriString += string.Format(CultureInfo.InvariantCulture, "/{0}/id.xml?api-key={1}", hash, this.ApiKey);

            var address = new Uri(uriString);

            var userIdClient = new WebClient();

            userIdClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(this.OnUserIdDownloaded);

            limiting.MakeWebRequest(userIdClient, address);
        }

        /// <summary>
        /// User ID has been downloaded
        /// </summary>
        /// <param name="sender">the user id web client</param>
        /// <param name="e">the download string completed event arguments</param>
        private void OnUserIdDownloaded(object sender, DownloadStringCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                System.Diagnostics.Debug.WriteLine(e.Error.Message);

                this.OnException(e.Error);

                return;
            }

            var doc = XDocument.Parse(e.Result);

            var element = doc.Descendants("user_id").FirstOrDefault();

            if (element != null)
            {
                this.UserId = long.Parse(element.Value, CultureInfo.InvariantCulture);
            }
        }

        /// <summary>
        /// Fetch the results
        /// </summary>
        private void FetchResults()
        {
            var uri = this.GenerateUri();

            if (uri == null)
            {
                return;
            }

            if (this.client != null)
            {
                this.client.CancelAsync();
            }

            this.IsLoading = true;

            this.client = new WebClient();

            this.client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(this.OnDownloadCompleted);

            System.Diagnostics.Debug.WriteLine(uri);

            limiting.MakeWebRequest(this.client, uri);
        }

        /// <summary>
        /// Result download completed event handler
        /// </summary>
        /// <param name="sender">the web client</param>
        /// <param name="e">the download string completed event arguments</param>
        private void OnDownloadCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            if (!e.Cancelled)
            {
                this.client = null;

                if (e.Error == null)
                {
                    var xml = e.Result;

                    this.Parse(xml);
                }
                else
                {
                    System.Diagnostics.Debug.WriteLine(e.Error.Message);

                    this.OnException(e.Error);
                }
            }

            this.IsLoading = false;
        }

        /// <summary>
        /// Generate the URI
        /// </summary>
        /// <returns>the service URI</returns>
        private Uri GenerateUri()
        {
            if (this.UserId <= 0)
            {
                return null;
            }

            if (string.IsNullOrEmpty(this.ApiKey))
            {
                return null;
            }

            var url = string.Format(CultureInfo.InvariantCulture, BaseApi, Version);

            url += string.Format(
                CultureInfo.InvariantCulture, 
                "/{0}/{1}.xml?api-key={2}",
                this.UserId, 
                this.DataType, 
                this.ApiKey);

            return new Uri(url, UriKind.Absolute);
        }

        #endregion
    }
}
